<?php
// $Id: to_do.module,v 1.6.2.17 2010/11/24 20:46:22 alexiswilke Exp $


// SAVED IN DATABASE, DO NOT CHANGE VALUES!
define('TO_DO_STATUS_NOT_STARTED',        0);
define('TO_DO_STATUS_STARTED',            1);
define('TO_DO_STATUS_FINISHED',           2);
define('TO_DO_STATUS_AWAITING_MORE_INFO', 3);
define('TO_DO_STATUS_DELAYED',            4);
define('TO_DO_STATUS_CANCELED',           5);
define('TO_DO_STATUS_IN_PROGRESS',        6);

// SAVED IN DATABASE, DO NOT CHANGE VALUES!
define('TO_DO_PRIORITY_LOW',       0);
define('TO_DO_PRIORITY_MEDIUM',    1);
define('TO_DO_PRIORITY_HIGH',      2);
define('TO_DO_PRIORITY_IMMEDIATE', 3);

// SELECTION
define('TO_DO_SELECTION_LAST_SELECTION', -1);
define('TO_DO_SELECTION_URGENT',          0);
define('TO_DO_SELECTION_ALL',             1);

// DEADLINE EVENTS
define('TO_DO_DEADLINE_EVENT_NONE',      0);
define('TO_DO_DEADLINE_EVENT_REMINDER', 10); // 10, 11 and 12 used at this time
define('TO_DO_DEADLINE_EVENT_PAST',     20);
define('TO_DO_DEADLINE_EVENT_TODAY',    30);


/**
 * Implementation of hook_menu().
 */
function to_do_menu() {
  module_load_include('admin.inc', 'to_do');
  return to_do_menu_array();
}

/**
 * Implementation of hook_node_info().
 */
function to_do_node_info() {
  return array(
    'to_do' => array(
      'name' => t('To do'),
      'module' => 'to_do',
      'description' => t('A to do item to be added to a user\'s list of things to do.'),
      'has_title' => TRUE,
      'title_label' => t('To do task'),
      'has_body' => TRUE,
      'body_label' => t('Assignment details'),
    )
  );
}

/**
 * Implementation of hook_perm().
 */
function to_do_perm() {
  // * standard CRUD permissions will be removed for drupal 7 (handled by core).
  //
  // IMPORTANT NOTE:
  // The "to do" should actually have been "to_do".
  // When moving to Drupal 7, have an update that fixes all the permission names
  // (and obviously you'll have to fix them here too, as required.)
  return array(
    'access all to do content',
    'administer to do list',
    'can be assigned to dos',
    'create to do content', // *
    'edit any assigned to do content',
    'edit any to do content', // *
    'edit own to do content', // *
    'delete any to do content', // *
    'delete own to do content', // *
  );
}

/**
 * Implementation of hook_access().
 *
 * This hook is deprecated in drupal 7.
 */
function to_do_access($op, $node, $account) {
  if ($op == 'create') {
    return user_access('create to do content', $account);
  }
}

/**
 * Implementation of hook_node_access_records().
 *
 * standard CRUD permissions need to be removed from here in drupal 7 (see to_do_perm())
 *
 */
function to_do_node_access_records($node) {
  $grants = array();
  if ($node->type == 'to_do') {
    // node author access permissions
    $author = user_load($node->uid);
    $grant_update = user_access('edit own to do content', $author)
      || user_access('edit any to do content', $author)
      || user_access('administer to do list', $author);
    $grant_delete = user_access('delete own to do content', $author)
      || user_access('delete any to do content', $author)
      || user_access('administer to do list', $author);
    $grants[] = array(
      'realm' => 'to_do_author',
      'gid' => $node->uid,
      'grant_view' => TRUE,
      'grant_update' => $grant_update,
      'grant_delete' => $grant_delete,
      'priority' => 0,
    );
    if (!empty($node->assigned_users)) {
      // access permissions for each assigned user
      foreach ($node->assigned_users as $uid => $ignore) {
        if ($uid > 0) {
          $assignee = user_load($uid);
          $grant_update = user_access('edit any assigned to do content', $assignee)
            || user_access('edit any to do content', $assignee)
            || user_access('administer to do list', $assignee);
          $grant_delete = user_access('delete any to do content', $assignee)
            || user_access('administer to do list', $assignee);
          $grants[] = array(
            'realm' => 'to_do_assignee',
            'gid' => $uid,
            'grant_view' => TRUE,
            'grant_update' => $grant_update,
            'grant_delete' => $grant_delete,
            'priority' => 0,
          );
        }
      }
    }
    // in this one we accept anonymous although it may not be a good idea
    // to let anonymous users see to do items...
    $all_to_dos = user_roles(FALSE, 'access all to do content');
    foreach ($all_to_dos as $rid => $role) {
      $grants[] = array(
        'realm' => 'to_do_access_content',
        'gid' => $rid,
        'grant_view' => TRUE,
        'grant_update' => FALSE,
        'grant_delete' => FALSE,
        'priority' => 0,
      );
    }
  }
  return $grants;
}

function to_do_node_grants($account, $op) {
  $grants = array();
  $grants['to_do_author'][] = $account->uid;
  $grants['to_do_assignee'][] = $account->uid;
  $grants['to_do_access_content'] = array_keys($account->roles);
  return $grants;
}

/**
 * Fix the start_date and deadline fields in case they are still
 * arrays or not set yet. We want to transform them to a valid
 * Unix date so all functions can deal with the data in a simple
 * way.
 *
 * When a deadline is defined and is now out of date, we also may
 * change the item status to TO_DO_STATUS_FINISHED.
 */
function _to_do_fix_dates(&$node) {
  global $user;

  // Important Note: this assumes that we always call format_date() without defining timezone
  if (variable_get('configurable_timezones', 1) && $user->uid && strlen($user->timezone)) {
    $timezone = $user->timezone;
  }
  else {
    $timezone = variable_get('date_default_timezone', 0);
  }

  // Start date logic
  if (isset($node->start_date)) {
    if (is_array($node->start_date)) {
      if (empty($node->include_start_date) || empty($node->start_date)) {
        // user does not want a start date (this is the default)
        $node->start_date = 0;
      }
      else {
        // date was defined in the form
        $node->start_date = mktime(
          0, 0, 0,
          $node->start_date['month'],
          $node->start_date['day'],
          $node->start_date['year']
        );
        $node->start_date -= $timezone;
      }
    }
  }
  else {
    $node->start_date = 0;
  }

  // Deadline logic
  if (isset($node->deadline)) {
    if (is_array($node->deadline)) {
      if (empty($node->include_deadline) || empty($node->deadline)) {
        // user does not want a start date (this is the default)
        $node->deadline = 0;
      }
      else {
        // date was defined in the form
        $node->deadline = mktime(
          0, 0, 0,
          $node->deadline['month'],
          $node->deadline['day'],
          $node->deadline['year']
        );
        $node->deadline -= $timezone;
      }
    }
  }
  else {
    $node->deadline = 0;
  }

  // just in case, also fix the item status
  if ($node->deadline != 0 && $node->auto_close && $node->deadline <= time()) {
    $node->item_status = TO_DO_STATUS_FINISHED;
  }
}

/**
 * Implementation of hook_form().
 */
function to_do_form(&$node, $form_state) {
  module_load_include('pages.inc', 'to_do');
  return _to_do_form($node, $form_state);
}

/**
 * Implementation of hook_validate().
 */
function to_do_validate($node, &$form) {
  module_load_include('pages.inc', 'to_do');
  return _to_do_validate($node, $form);
}

/**
 * Implementation of hook_insert().
 */
function to_do_insert($node) {
  module_load_include('pages.inc', 'to_do');
  return _to_do_insert($node);
}

/**
 * Implementation of hook_update().
 */
function to_do_update($node) {
  module_load_include('pages.inc', 'to_do');
  return _to_do_update($node);
}

/**
 * Implementation of hook_delete().
 */
function to_do_delete($node) {
  db_query('DELETE FROM {to_do} WHERE nid = %d', $node->nid);
  db_query('DELETE FROM {to_do_assigned_users} WHERE nid = %d', $node->nid);
}

/**
 * Implementation of hook_nodeapi().
 */
function to_do_nodeapi(&$node, $op, $teaser, $page) {
  if ($op == 'delete revision') {
    db_query('DELETE FROM {to_do} WHERE vid = %d', $node->vid);
    db_query('DELETE FROM {to_do_assigned_users} WHERE vid = %d', $node->vid);
  }
}

/**
 * Implementation of hook_load().
 */
function to_do_load($node) {
  global $user;

  if (isset($_SESSION['redirect_todo'])) {
    $destination = drupal_get_path_alias($_SESSION['redirect_todo']);
    unset($_SESSION['redirect_todo']);
    drupal_goto($destination);
    /*NOTREACHED*/
  }

  $sql = "SELECT item_status, priority, start_date, deadline, date_finished,"
      . " deadline_event, auto_close, mark_permissions"
    . " FROM {to_do} WHERE vid = %d";
  $result = db_query($sql, $node->vid);
  $return_object = db_fetch_object($result);
  if (empty($return_object)) {
    // this should never happen, but just in case... it works better with
    // correct defaults.
    $return_object = new stdClass;
    $return_object->item_status = TO_DO_STATUS_STARTED;
    $return_object->priority = TO_DO_PRIORITY_MEDIUM;
    $return_object->start_date = 0;
    $return_object->deadline = 0;
    $return_object->date_finished = NULL;
    $return_object->deadline_event = 0;
    $return_object->auto_close = 0;
    $return_object->mark_permissions = 0;
  }

  $return_object->assigned_users = array();
  $sql = 'SELECT tdau.uid, u.name FROM {to_do_assigned_users} AS tdau ' .
                                'JOIN {users} AS u ON tdau.uid = u.uid ' .
                      'WHERE tdau.vid = %d';
  $result = db_query($sql, $node->vid);
  while ($o = db_fetch_array($result)) {
    if ($o['uid']) {
      $return_object->assigned_users[$o['uid']] = $o['name'];
    }
  }

  $return_object->self = !empty($return_object->assigned_users[$user->uid]);
  if ($return_object->self) {
    // emphasis self (is that really the correct location for this feature?!)
    $return_object->assigned_users[$user->uid] = '<em>' . $return_object->assigned_users[$user->uid] . '</em>';
  }

  return $return_object;
}

/**
 * Implementation of hook_view().
 */
function to_do_view($node, $teaser = FALSE, $page = FALSE) {
  global $user;

  $path = drupal_get_path('module', 'to_do');
  drupal_add_css($path . '/css/to_do_node.css');

  _to_do_fix_dates($node);

  $node = node_prepare($node, $teaser);

  $date_format = variable_get('to_do_date_format', 'Y/n/j');

  // current status
  $item_status = _to_do_status_list($node->item_status);
  if ($node->date_finished) {
    $item_status .= ' (' . format_date($node->date_finished, 'custom', $date_format) .')';
  }

  // when the TO DO starts
  if ($node->start_date) {
    $node->start_date = format_date($node->start_date, 'custom', $date_format);
  }

  // when the TO DO ends
  if ($node->deadline) {
    $node->deadline = format_date($node->deadline, 'custom', $date_format);
    if ($node->auto_close) {
      $node->deadline .= ' (*)';
    }
  }

  $description_title = variable_get('to_do_description_title', t('Details'));

  $fieldset_title = variable_get('to_do_fieldset_title', t('To Do Listing'));
  if ($fieldset_title) {
    $collapsed = $teaser && $node->priority < TO_DO_PRIORITY_HIGH;
    $node->content['to_do_fieldset'] = array(
      '#type' => 'fieldset',
      '#title' => $fieldset_title,
      '#collapsible' => $teaser,
      '#collapsed' => $collapsed,
      '#attributes' => array('class' => 'to-do-fieldset'),
    );
  }
  else {
    $node->content['to_do_fieldset'] = array();
  }

  $node->content['to_do_fieldset']['header'] = array(
    '#value' => theme('to_do_header', $node, $description_title),
    '#weight' => -100,
  );

  $node->content['to_do_fieldset']['to_do_body'] = array(
    '#value' => theme(
      'to_do_body',
      $node->content['body']['#value'],
      $description_title
    ),
    '#weight' => 0,
  );

  // XXX -- we could have an interface with a table for this one:
  //        when in this state -> show that button (drop down selection)
  $action_buttons = FALSE;
  if (variable_get('to_do_show_action_button', 1)
      && !$node->date_finished
      && ($node->uid == $user->uid || $node->mark_permissions || user_access('administer to do list'))) {
    if ($node->item_status == TO_DO_STATUS_NOT_STARTED) {
      $action_buttons = drupal_get_form('to_do_start_button_form', $node->nid);
    }
    else {
      $action_buttons = drupal_get_form('to_do_finished_button_form', $node->nid);
    }
  }

  $navigation_elements = to_do_navigation_buttons_form($node->nid);
  $navigation_buttons = drupal_render($navigation_elements);

  if (variable_get('to_do_show_info_after', 1)) {
    $weight = 1; // goes right after the body
  }
  else {
    $weight = -3; // goes right before the body
  }
  $node->content['to_do_fieldset']['to_do_info'] = array(
    '#value' => theme('to_do_info', $item_status,
      _to_do_priority_list($node->priority), $node->start_date,
      $node->deadline, $node->assigned_users, $action_buttons,
      variable_get('to_do_side_actions', 0), $description_title,
      $navigation_buttons),
    '#weight' => $weight,
  );

  $node->content['to_do_fieldset']['footer'] = array(
    '#value' => theme('to_do_footer', $node),
    '#weight' => 100,
  );

  // mark the body as empty, otherwise the system adds it again for us!
  $node->content['body'] = array(
    '#value' => '',
  );

  return $node;
}

/**
 * Implementation of hook_user().
 */
function to_do_user($op, &$edit, &$account, $category = NULL) {
  module_load_include('users.inc', 'to_do');
  return _to_do_user($op, $edit, $account);
}

/**
 * Implementation of hook_theme().
 */
function to_do_theme() {
  $path = drupal_get_path('module', 'to_do') . '/templates';
  return array(
    'to_do_header' => array(
      'arguments' => array(
        'to_do' => NULL,
        'description_title' => NULL,
      ),
    ),
    'to_do_body' => array(
      'arguments' => array(
        'body' => NULL,
        'description_title' => NULL,
      ),
      'path' => $path,
      'template' => 'to_do_body',
    ),
    'to_do_info' => array(
      'arguments' => array(
        'item_status' => NULL,
        'priority' => NULL,
        'start_date' => NULL,
        'deadline' => NULL,
        'users' => NULL,
        'action_buttons' => NULL,
        'side_actions' => NULL,
        'description_title' => NULL,
        'navigation_buttons' => NULL,
      ),
      'path' => $path,
      'template' => 'to_do_info',
    ),
    //'to_do_footer' => array(
    //  'arguments' => array(
    //    'to_do' => NULL,
    //  ),
    //),
  );
}

/**
 * Implementation of theme_hook().
 */
function theme_to_do_header($node, $description_title) {
  return '<h2>' . $description_title . '</h2>';
}


/**
 * Implementation of hook_cron().
 */
function to_do_cron() {
  $now = time() / 86400; // what is today?
  $last_update = variable_get('to_do_last_update', 0);
  if ($last_update < $now) {
    variable_set('to_do_last_update', $now);
    $now *= 86400;

    $reminder = array();
    for ($i = 1; $i <= 3; ++$i) {
      $reminder[$i] = variable_get('to_do_reminder' . $i, '');
      if (!$reminder[$i]) {
        unset($reminder[$i]);
      }
      else {
        $reminder[$i] *= 86400;
      }
    }
    sort($reminder, SORT_NUMERIC);
    $first = end($reminder);

    // Check for reminders, deadline reached & past
    $sql = "SELECT n.nid, td.deadline, td.deadline_event FROM {to_do} td, {node} n"
      . " WHERE auto_close = 0 AND %d > deadline AND td.item_status <> %d AND td.item_status <> %d"
        . " AND td.nid = n.nid AND td.vid = n.vid AND td.deadline_event <> %d";
    $result = db_query($sql, $now - $first, TO_DO_STATUS_FINISHED, TO_DO_STATUS_CANCELED,
      TO_DO_DEADLINE_EVENT_PAST);
    while ($row = db_fetch_array($result)) {
      $node = node_load($row['nid']);
      if ($now > $row['deadline']) { // already past
        // this test should always return TRUE since we skip those nodes
        if ($row['deadline_event'] != TO_DO_DEADLINE_EVENT_PAST) {
          $sql = "UPDATE {to_do} SET deadline_event = %d WHERE nid = %d";
          db_query($sql, TO_DO_DEADLINE_EVENT_PAST, $row['nid']);
          module_invoke_all('to_do_past_deadline', 'to_do', $node);
        }
      }
      elseif ($now == $row['deadline']) { // today!
        if ($row['deadline_event'] != TO_DO_DEADLINE_EVENT_TODAY) {
          $sql = "UPDATE {to_do} SET deadline_event = %d WHERE nid = %d";
          db_query($sql, TO_DO_DEADLINE_EVENT_TODAY, $row['nid']);
          module_invoke_all('to_do_deadline_reached', 'to_do', $node);
        }
      }
      else {
        $i = count($reminder);
        while ($i > 0) {
          --$i;
          if ($now - $reminder[$i] >= $row['deadline']
              && $row['deadline_event'] < TO_DO_DEADLINE_EVENT_REMINDER + (2 - $i)) {
            $sql = "UPDATE {to_do} SET deadline_event = %d WHERE nid = %d";
            db_query($sql, TO_DO_DEADLINE_EVENT_REMINDER + (2 - $i), $row['nid']);
            module_invoke_all('to_do_reminder', 'to_do', $node, 2 - $i, $reminder[$i] / 86400);
          }
        }
      }
    }

    // Mark finished when auto-close is set and deadline is past
    $sql = "SELECT n.nid FROM {to_do} td, {node} n"
      . " WHERE auto_close = 1 AND %d > deadline AND td.item_status <> %d AND td.item_status <> %d"
        . " AND td.nid = n.nid AND td.vid = n.vid";
    $result = db_query($sql, $now, TO_DO_STATUS_FINISHED, TO_DO_STATUS_CANCELED);
    while ($row = db_fetch_array($result)) {
      to_do_mark_finished($nid);
    }

    // Mark unpublished when 'unpublish after' is set
    $unpublish_after = variable_get('to_do_unpublish_after', '');
    if (is_numeric($unpublish_after) && $unpublish_after >= 0) {
      $unpublish_after *= 86400;
      $sql = "SELECT n.nid FROM {to_do} td, {node} n"
        . " WHERE n.type = 'to_do' AND %d > td.date_finished AND n.status = 1"
          . " AND n.nid = td.nid AND n.vid = td.vid";
      $result = db_query($sql, $now + $unpublish_after);
      while ($row = db_fetch_array($result)) {
        $node = node_load($row['nid']);
        $node->status = 0;
        node_save($node);
        module_invoke_all('to_do_unpublished', 'to_do', $node);
      }
    }

    // Delete when 'delete after' is set
    // Note: We could run DELETE before UNPUBLISH, but some Rules could
    //       make use the the 'unpublished' event... that would otherwise
    //       to occur!
    $delete_after = variable_get('to_do_delete_after', '');
    if (is_numeric($delete_after) && $delete_after > 0) {
      $delete_after *= 86400;
      $sql = "SELECT n.nid FROM {to_do} td, {node} n"
        . " WHERE type = 'to_do' AND %d > date_finished"
          . " AND n.nid = td.nid AND n.vid = td.vid";
      if (!variable_get('to_do_delete_any', 0)) {
        $sql .= " AND n.status = 0";
      }
      $result = db_query($sql, $now + $delete_after);
      while ($row = db_fetch_array($result)) {
        $node = node_load($row['nid']);
        module_invoke_all('to_do_deleting', 'to_do', $node);
        node_delete($row['nid']);
      }
    }
  }
}

/**
 * Access callback function for the redirect from to_do to to_do/%/user
 */
function _to_do_redirect_access_callback() {
  global $user;

  return $user->uid == 1
      || user_access('administer to do list')
      || user_access('can be assigned to dos')
      || user_access('create to do content');
}

/**
 * Access callback function for the auto complete list at to_do/autocomplete/user
 */
function _to_do_access_autocomplete_callback() {
  global $user;

  return $user->uid == 1
      || user_access('edit any to do content')
      || user_access('create to do content')
      || user_access('administer to do list');
}

/**
 * Callback function to determine who can view the following paths:
 * + user/%/to_do
 * + user/%/to_do/list
 * + user/%/to_do/all
 * + user/%/to_do/created
 */
function _to_do_access_callback($uid) {
  global $user;

  if ($user->uid == 1 || user_access('administer to do list')) {
    return TRUE;
  }
  if ($user->uid == $uid
      && (user_access('can be assigned to dos') || user_access('create to do content'))) {
    return TRUE;
  }
  return FALSE;
}

/**
 * Returns a list of possible statuses for the node
 */
function _to_do_status_list($index = FALSE) {
  static $status;

  if (!isset($status)) {
    $status = array(
      TO_DO_STATUS_NOT_STARTED        => t('Not Started'),
      TO_DO_STATUS_STARTED            => t('Started'),
      TO_DO_STATUS_FINISHED           => t('Finished'),
      TO_DO_STATUS_AWAITING_MORE_INFO => t('Awaiting More Information'),
      TO_DO_STATUS_DELAYED            => t('Delayed'),
      TO_DO_STATUS_CANCELED           => t('Cancelled'),
      TO_DO_STATUS_IN_PROGRESS        => t('In Progress'),
    );
  }

  if ($index !== FALSE) {
    return $status[$index];
  }
  return $status;
}

/**
 * Returns a list of possible priorities for the node
 */
function _to_do_priority_list($index = FALSE) {
  static $priority;

  if (!isset($priority)) {
    $priority = array(
      TO_DO_PRIORITY_LOW       => t('Low'),
      TO_DO_PRIORITY_MEDIUM    => t('Medium'),
      TO_DO_PRIORITY_HIGH      => t('High'),
      TO_DO_PRIORITY_IMMEDIATE => t('Immediate'),
    );
  }

  if ($index !== FALSE) {
    if (isset($priority[$index])) {
      return $priority[$index];
    }
    // when using views, we may enter this with an index
    // that is not FALSE and not a valid 1 to x number
    return t('Undefined');
  }
  return $priority;
}

/**
 * Returns a list of roles that have 'assign to do list' permissions
 */
function _to_do_roles() {
  return user_roles(TRUE, 'can be assigned to dos');
}

/**
 * Returns the default value for the roles on the node edit form
 */
function _to_do_selected_roles($node, $form_state) {
  if ($form_state['submitted'] && $form_state['values']['roles']) {
    $default_value = $form_state['values']['roles'];
  }
  elseif (isset($node->nid) && !$form_state['submitted']) {
    $default_value = array();
    $roles = _to_do_roles();
    foreach ($roles as $key => $ro) {
      if (isset($node->roles[$key])) {
        $default_value[$key] = $key;
      }
      else {
        $default_value[$key] = 0;
      }
    }
    arsort($default_value, SORT_NUMERIC);
  }
  else {
    $default_value = array();
  }
  return $default_value;
}

/**
 * Function to determine if any roles have been selected for the node
 */
function _to_do_selected_roles_exist($node, $form_state) {
  if ($form_state['submitted'] && is_array($form_state['values']['roles'])) {
    foreach ($form_state['values']['roles'] as $val) {
      if ($val > 0) {
        return TRUE;
      }
    }
  }
  elseif (isset($node->roles)) {
    foreach ($node->roles as $ro => $ignore) {
      if ($ro > 0) {
        return TRUE;
      }
    }
  }

  return FALSE;
}

/**
 * Return the list of posible selections.
 */
function _to_do_single_column_list($index = FALSE) {
  $items = array(
    TO_DO_SELECTION_URGENT  => t('Urgent'),
    TO_DO_SELECTION_ALL     => t('All'),
  );
  if ($index !== FALSE && $index >= 0 && $index < count($items)) {
    return $items[$index];
  }
  return $items;
}

/**
 * Helper function used to generate a date or '---' if the date is not
 * defined.
 */
function to_do_format_date($date) {
  static $date_format;

  if ($date) {
    if (!isset($date_format)) {
      $date_format = variable_get('to_do_date_format', 'Y/n/j');
    }
    return format_date($date, 'custom', $date_format);
  }

  return '---';
}

/**
 * Helper function transforming an item status in a class name so we can
 * change the colors via CSS depending on the current status.
 */
function to_do_item_status_class($list_item) {
  if ($list_item['item_status'] == TO_DO_STATUS_FINISHED) {
    return 'to-do-finished';
  }
  if ($list_item['deadline'] && $list_item['deadline'] <= time()) {
    if (floor($list_item['deadline'] / 86400) < floor(time() / 86400)) {
      return 'to-do-late'; // past due
    }
    return 'to-do-now'; // due today
  }

  switch ($list_item['priority']) {
  case TO_DO_PRIORITY_LOW:
    return 'to-do-low';

  case TO_DO_PRIORITY_HIGH:
    return 'to-do-high';

  case TO_DO_PRIORITY_IMMEDIATE:
    return 'to-do-immediate'; // ASAP

  default: //case TO_DO_PRIORITY_MEDIUM:
    return 'to-do-medium';

  }
}

/**
 * Create a next/previous set of buttons for easy navigation.
 */
function to_do_navigation_buttons_form($nid) {
  $form = array();

  $np = to_do_next_previous_nodes($nid);

  if ($np['next']['nid']) {
    $form['next_to_do'] = array(
      '#value' => t('Next to do') . ': ' . l($np['next']['title'], 'node/' . $np['next']['nid'],
        array('id' => 'next_to_do', 'title' => t('Next to do item'))),
      '#prefix' => '<li>',
      '#suffix' => '</li>',
      '#weight' => 2,
    );
  }

  if ($np['previous']['nid']) {
    $form['previous_to_do'] = array(
      '#value' => t('Previous to do') . ': ' . l($np['previous']['title'], 'node/' . $np['previous']['nid'],
        array('id' => 'previous_to_do', 'title' => t('Previous to do item'))),
      '#prefix' => '<li>',
      '#suffix' => '</li>',
      '#weight' => 1,
    );
  }

  return $form;
}

/**
 * Search for the next and previous to do nodes from a given to do node.
 *
 * The order is what you'd expect by default in the block.
 * (i.e. what you should be working on next and should have
 * been working on before.)
 *
 * The result is an array with two entries: 'next' and 'previous'.
 *
 * Each entry is an array with two entries: 'nid' and 'title'.
 * (the next and previous may have other fields, however, we
 * really only guarantee 'nid' and 'title'. So do not use the
 * other fields.)
 *
 * @param[in] $nid The node identifier to work on.
 */
function to_do_next_previous_nodes($nid) {
  // we cache results because we may hit the same $nid multiple times
  // (because of the node being viewed, the block, the buttons...)
  static $results = array();

  if (!isset($results[$nid])) {

    // default array
    $results[$nid] = array(
      'next' => array(
        'nid' => 0,
        'title' => '',
      ),
      'previous' => array(
        'nid' => 0,
        'title' => '',
      ),
    );

    // get the current node
    $sql = "SELECT td.deadline FROM {to_do} td, {node} n"
      . " WHERE n.status = 1 AND n.nid = td.nid AND n.vid = td.vid"
        . " AND n.type = 'to_do' AND n.nid = %d";
    $to_do = db_fetch_array(db_query(db_rewrite_sql($sql), $nid));
    if ($to_do) {
      // ignore canceled & finished tasks (i.e. item_status 2 and 5)
      //
      // The SQL commands to search the next and previous may return
      // multiple entries because they all have the same deadline.
      // So if you create 3 To do items all with the same deadline
      // they will be sorted on priority then title. Say we have:
      //
      // 1. A
      // 2. B
      // 3. C
      //
      // When we search for the next for B, we first find A, then B
      // and finally C. Our while loops below set $found to true when
      // we find B and return the next item: C.
      //
      // Similaraly, the previous from B will first find C, then B
      // which sets $found to TRUE and thus we return the following
      // entry which is A.
      //
      // If the deadline is different for each entry, the first entry
      // we get is $nid and thus $found is set to TRUE immediately.

      // WARNING: For PostgreSQL, the SELECT and ORDER BY must have all the same
      // items (that's because the db_rewrite_sql() adds a DISTINCT after SELECT.)
      $sql = "SELECT n.nid, n.title, td.priority, td.deadline FROM {to_do} td, {node} n"
        . " WHERE n.status = 1 AND n.nid = td.nid AND n.vid = td.vid"
          . " AND n.type = 'to_do' AND td.deadline <= %d"
          . " AND (td.item_status NOT IN (2, 5) OR n.nid = %d)"
        . " ORDER BY td.deadline DESC, td.priority, n.title DESC, n.nid DESC";
      $r = db_query(db_rewrite_sql($sql), $to_do['deadline'], $nid);
      $found = FALSE;
      while ($previous = db_fetch_array($r)) {
        if ($found) {
          $results[$nid]['previous'] = $previous;
          break;
        }
        else {
          $found = $previous['nid'] == $nid;
        }
      }

      // WARNING: For PostgreSQL, the SELECT and ORDER BY must have all the same
      // items (that's because the db_rewrite_sql() adds a DISTINCT after SELECT.)
      $sql = "SELECT n.nid, n.title, td.priority, td.deadline FROM {to_do} td, {node} n"
        . " WHERE n.status = 1 AND n.nid = td.nid AND n.vid = td.vid"
          . " AND n.type = 'to_do' AND td.deadline >= %d"
          . " AND (td.item_status NOT IN (2, 5) OR n.nid = %d)"
        . " ORDER BY td.deadline, td.priority DESC, n.title, n.nid";
      $r = db_query(db_rewrite_sql($sql), $to_do['deadline'], $nid);
      $found = FALSE;
      while ($next = db_fetch_array($r)) {
        if ($found) {
          $results[$nid]['next'] = $next;
          break;
        }
        else {
          $found = $next['nid'] == $nid;
        }
      }
    }
  }

  return $results[$nid];
}

/**
 * Provides a button that is attached to the node form to mark
 * the To do list item as started.
 */
function to_do_start_button_form($form_state, $nid) {
  $form['nid'] = array(
    '#type' => 'value',
    '#value' => $nid,
  );
  $form['mark_started'] = array(
    '#type' => 'submit',
    '#value' => t('Mark Started'),
  );
  return $form;
}

/**
 * Submit function for the start button form.
 */
function to_do_start_button_form_submit($form, &$form_state) {
  $node = node_load($form_state['values']['nid']);
  if ($node && $node->item_status == TO_DO_STATUS_NOT_STARTED) {
    // mark item as started
    $node->item_status = TO_DO_STATUS_STARTED;
    $node->start_date = time();
    node_save($node);
  }
}

/**
 * Provides a button that is attached to the node form to mark
 * the To do list item as finished.
 */
function to_do_finished_button_form($form_state, $nid) {
  $form['nid'] = array(
    '#type' => 'value',
    '#value' => $nid,
  );
  $form['mark_finished'] = array(
    '#type' => 'submit',
    '#value' => t('Mark Finished'),
  );
  return $form;
}

/**
 * submit function for the finished button form
 */
function to_do_finished_button_form_submit($form, &$form_state) {
  global $user;

  $nid = $form_state['values']['nid'];
  to_do_mark_finished($nid);

  $goto_nid = 0;
  $list = explode(',', str_replace(' ', '', variable_get('to_do_mark_finished_actions', '')));
  foreach ($list as $action) {
    switch ($action) {
    case 'next':
      $np = to_do_next_previous_nodes($nid);
      $goto_nid = $np['next']['nid'];
      break;

    case 'previous':
      $np = to_do_next_previous_nodes($nid);
      $goto_nid = $np['previous']['nid'];
      break;

    case 'first':
      // WARNING: For PostgreSQL, the SELECT and ORDER BY must have all the same
      // items (that's because the db_rewrite_sql() adds a DISTINCT after SELECT.)
      $sql = "SELECT n.nid, td.deadline, td.priority, n.title FROM {to_do} td, {node} n"
        . " WHERE n.status = 1 AND n.nid = td.nid AND n.vid = td.vid"
          . " AND n.type = 'to_do' AND td.item_status NOT IN (2, 5)"
        . " ORDER BY td.deadline, td.priority DESC, n.title, n.nid";
      $r = db_query(db_rewrite_sql($sql));
      if ($row = db_fetch_array($r)) {
        $goto_nid = $row['nid'];
      }
      break;

    case 'last':
      // WARNING: For PostgreSQL, the SELECT and ORDER BY must have all the same
      // items (that's because the db_rewrite_sql() adds a DISTINCT after SELECT.)
      $sql = "SELECT n.nid, td.deadline, td.priority, n.title FROM {to_do} td, {node} n"
        . " WHERE n.status = 1 AND n.nid = td.nid AND n.vid = td.vid"
          . " AND n.type = 'to_do' AND td.item_status NOT IN (2, 5)"
        . " ORDER BY td.deadline DESC, td.priority, n.title DESC, n.nid DESC";
      $r = db_query(db_rewrite_sql($sql));
      if ($row = db_fetch_array($r)) {
        $goto_nid = $row['nid'];
      }
      break;

    case 'front':
      drupal_goto();
      /*NOTREACHED*/

    case 'list':
      if ($user->uid != 0) {
        drupal_goto('user/' . $user->uid . '/to_do');
        /*NOTREACHED*/
      }
      break;

    default:
      // user specified path
      if ($action[0] == '/') {
        drupal_goto($action[0]);
      }
      // anything else is ignored, although there shouldn't be such a thing
      break;

    }

    if ($goto_nid) {
      drupal_goto('node/' . $goto_nid);
      /*NOTREACHED*/
    }
  }
}

/**
 * Mark a To do item as finished.
 */
function to_do_mark_finished($nid) {
  $node = node_load($nid);
  if ($node && $node->item_status != TO_DO_STATUS_FINISHED
      && $node->item_status != TO_DO_STATUS_CANCELED) {
    // the node_save() calls our to_do_update() function
    $node->item_status = TO_DO_STATUS_FINISHED;
    $node->sticky = 0;
    node_save($node);
    $node = node_load($nid, NULL, TRUE); // reset the static node

    // we do take care of the status flag after because we do want
    // 2 distinct steps
    if ($node->status) {
      $unpublish_after = variable_get('to_do_unpublish_after', '');
      if (is_numeric($unpublish_after) && $unpublish_after == 0 && $node->status) {
        // immediate unpublish
        $node->status = 0;
        node_save($node);
        $node = node_load($nid, NULL, TRUE); // reset the static node

        module_invoke_all('to_do_unpublished', 'to_do', $node);
      }
    }
  }
}

// vim: ts=2 sw=2 et syntax=php
